package com.example.services.impl;

import com.example.dto.Result;
import com.example.entity.SeckillVoucher;
import com.example.entity.VoucherOrder;
import com.example.mapper.VoucherOrderMapper;
import com.example.services.ISeckillVoucherService;
import com.example.services.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.utils.RedisIDWorker;
import com.example.utils.UserHolder;
import jakarta.annotation.Resource;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

/**
 * 服务实现类
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    /**
     * ISeckillVoucherService代理对象
     */
    @Resource
    private ISeckillVoucherService seckillVoucherService;

    /**
     * RedisIDWorker代理对象
     */
    @Resource
    private RedisIDWorker redisIDWorker;

    @Override
    public Result seckillVoucher(Long voucherId) {
        /**
         * 查询并校验秒杀券状态
         */
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始！");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 已经结束
            return Result.fail("秒杀已经结束！");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足！");
        }
//        // 5.一人一单
//        Long userId = UserHolder.getUser().getId();
//        // 5.1.查询订单
//        Long count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//        // 5.2.判断是否存在
//        if (count > 0) {
//            // 用户已经购买过了
//            return Result.fail("用户已经购买过一次了！");
//        }
//        // 6.扣减库存
//        boolean success = seckillVoucherService.update()
//                .setSql("stock = stock - 1") // set stock = stock - 1
//                .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
//                .update();
//        if (!success) {
//            // 扣减失败
//            return Result.fail("库存不足！");
//        }
//        // 7.创建订单
//        VoucherOrder voucherOrder = new VoucherOrder();
//        // 7.1.订单id
//        long orderId = redisIDWorker.nextID("order");
//        voucherOrder.setId(orderId);
//        // 7.2.用户id
//        voucherOrder.setUserId(userId);
//        // 7.3.代金券id
//        voucherOrder.setVoucherId(voucherId);
//        save(voucherOrder);
//        // 8.返回订单id
//        return Result.ok(orderId);
        /**
         * 由于@Transactional是加在createSecKillVoucherOrder()上，而不是加在seckillVoucher()上。
         * 这里使用this.createSecKillVoucherOrder()调用，this是当前的VoucherOrderServiceImpl对象（目标对象），而不是它的代理对象。
         * 我们知道事务要想生效，其实是Spring对当前的VoucherOrderServiceImpl对象做了动态代理（Spring默认使用JDK动态代理，即对接口做代理，
         * 所以代理对象为IVoucherOrderService接口），拿到代理对象后去做事务处理。而当前的this非代理对象，而是目标对象，不具有事务功能。
         * 当前对象this是目标对象不具有事务功能，代理对象proxy是Spring使用JDK动态代理生成具有事务功能
         */
        /**
         * synchronized是本地锁，只能保证单个JVM内部多个线程之间的互斥。由于现在我们部署了多个tomcat节点，每个tomcat都有一个属于自己的JVM，每个JVM都有自己的堆、栈、方法区和常量池。
         * 每个JVM都有一把synchronized锁，在JVM内部是一个锁监视器，多个JVM就有多个锁监视器，导致每一个锁都可以有一个线程获取，于是从原来的本地互斥锁变成了并行执行，就会发送并发安全问题。
         * 这就是在集群环境或分布式系统下，synchronized锁失效的原因，在这种情况下，我们就需要使用分布式锁（跨JVM锁、跨进程锁）来解决这个问题，让多个JVM只能使用同一把锁。
         */
        /**
         * 控制一人一单（关键步骤）
         */
        // 判断是否是一人一单，如果是再去下订单
        Long userId = UserHolder.getUser().getId();
        // intern()方法才能保证每个用户的锁对象唯一
        /**
         * 使用用户 ID 创建一把本地锁，intern() 会让相同字符串共享同一个内存地址，从而确保锁的唯一性。
         * 只能保证当前 JVM 中线程安全，不能解决分布式部署中的并发问题，为集群下的分布式锁埋下了扩展点。
         */
        synchronized (userId.toString().intern()) {
            /**
             * 使用AopContext.currentProxy()方法拿到当前目标对象的代理对象IVoucherOrderService接口，该代理对象被Spring管理，因此使用带有事务功能的代理对象去调用createSecKillVoucherOrder()方法。
             */
            // 获取代理对象（事务）
            // 使用AopContext.currentProxy()方法拿到当前目标对象的代理对象
            /**
             * 打断点出
             */
            /**
             * Spring 事务通过代理对象触发。
             * 如果你直接写 this.createVoucherOrder()，Spring AOP 不会生效，事务不生效。
             * 因此通过 AopContext.currentProxy() 获取当前对象的代理对象（具备事务能力），再调用方法。
             * ❗ 难点分析：
             * this 是当前目标对象，本身没有事务能力；Spring 会使用代理（JDK 动态代理或 CGLIB）添加事务。
             * @Transactional 注解放在 createVoucherOrder 方法上，仅在代理对象调用时生效。
             * AopContext.currentProxy() 必须在 Spring AOP 环境下启用，需要配置 exposeProxy=true。
             */
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            // 使用带有事务功能的代理对象去调用
            return proxy.createVoucherOrder(voucherId);
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        // 5.一人一单
        Long userId = UserHolder.getUser().getId();
        // 5.1.查询订单
        Long count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        /**
         * 代码调试：只有一个结果的，把断点打到if判断这里
         */
        if (count > 0) {
            // 用户已经购买过了
            return Result.fail("用户已经购买过一次了！");
        }
        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣减失败
            return Result.fail("库存不足！");
        }
        // 7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1.订单id
        long orderId = redisIDWorker.nextID("order");
        voucherOrder.setId(orderId);
        // 7.2.用户id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        // 8.返回订单id
        return Result.ok(orderId);
    }

}
/**
 * 难点/重点	        说明
 * 事务代理机制	    直接调用 this.方法() 不会触发事务；必须通过代理对象。
 * 本地锁的局限性	    synchronized 只能保证当前 JVM 中线程互斥，在集群环境中会失效。建议后续用 Redis 分布式锁等方案替代。
 * 库存扣减的并发控制	使用了 SQL 级别的条件判断 gt("stock", 0)，避免超卖。
 * 一人一单检查	    实现防止重复下单，是典型的秒杀业务需求。
 * 代理获取方法	    AopContext.currentProxy() 需开启 expose-proxy=true，否则会抛异常。
 * ID生成策略	    通过 Redis 实现全局唯一订单 ID（适用于分布式系统）。
 */
